// Copyright (c) 2023 Huawei Device Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// TODO: Serialization

use crate::structure::XmlDecl;
use crate::{Attribute, CdataSection, ChildNode, Comment, Document, Element, Text, XmlError};
use std::io::Write;

macro_rules! write_seq {
    ($writer: expr, $($content: expr),* $(,)?) => {
        $(
            $writer.write_all($content)?;
        )*
    };
}

/// An auxiliary struct that implements io::Write Trait.
struct AuxiliaryWriter {
    output: Vec<u8>,
}

impl AuxiliaryWriter {
    /// Creates a new instance of AuxiliaryWriter.
    fn new() -> Self {
        AuxiliaryWriter { output: Vec::new() }
    }
}

impl Write for AuxiliaryWriter {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.output.extend_from_slice(buf);
        Ok(buf.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

impl Document {
    /// Method to serialize the Document structure into a compact string.
    pub fn to_compact_string(&self) -> Result<String, XmlError> {
        let mut writer = AuxiliaryWriter::new();
        self.output(&mut writer)?;
        Ok(unsafe { String::from_utf8_unchecked(writer.output) })
    }
}

/// Trait for XML structures that can be converted to compact strings.
pub trait CompactOutput {
    /// Outputs the XML structure as a compact string.
    /// Returns an `XmlError` if writing to the `Write` trait object fails.
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError>;
}

impl CompactOutput for XmlDecl {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        // XMLDecl  ::= '<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>'
        // VersionInfo  ::= S 'version' Eq ("'" VersionNum "'" | '"' VersionNum '"')
        // EncodingDecl ::= S 'encoding' Eq ('"' EncName '"' | "'" EncName "'" )
        // SDDecl   ::= S 'standalone' Eq (("'" ('yes' | 'no') "'") | ('"' ('yes' |
        // 'no') '"'))
        write_seq!(writer, b"<?xml version=\"", self.version.as_bytes(), b"\"");
        if let Some(ref encoding) = self.xml_encoding {
            write_seq!(writer, b" encoding=\"", encoding.as_str().as_bytes(), b"\"");
        }
        if let Some(standalone) = self.standalone {
            write_seq!(
                writer,
                b" standalone=\"",
                if standalone { b"yes" } else { b"no" },
                b"\""
            );
        }
        writer.write_all(b"?>")?;
        Ok(())
    }
}

impl CompactOutput for ChildNode {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        match self {
            ChildNode::Comment(comment) => {
                comment.output(writer)?;
            }
            ChildNode::Element(element) => {
                element.output(writer)?;
            }
            ChildNode::Text(text) => {
                text.output(writer)?;
            }
            ChildNode::CdataSection(cdata_section) => {
                cdata_section.output(writer)?;
            }
        }
        Ok(())
    }
}

impl CompactOutput for Attribute {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        let attribute_inner = (*self.inner).borrow();
        write_seq!(writer, attribute_inner.name.as_bytes(), b"=\"",);
        write_content_replace_escape(writer, attribute_inner.val.clone())?;
        writer.write_all(b"\"")?;
        Ok(())
    }
}

impl CompactOutput for Document {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        let document_inner = (*self.inner).borrow();
        // Outputs xmldecl section。
        document_inner.decl.output(writer)?;

        // Outputs all childnodes in list.
        let list = (*document_inner.children.list).borrow();
        for node in list.iter() {
            node.output(writer)?;
        }
        Ok(())
    }
}

impl CompactOutput for Element {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        let element_inner = (*self.inner).borrow();
        // Outputs start tag.
        write_seq!(writer, b"<", element_inner.name.as_bytes());

        let map = (*element_inner.attributes.map).borrow();
        let mut attribute_num = 0;
        for (_key, attribute) in map.iter() {
            attribute_num += 1;
            if attribute_num > 0 {
                writer.write_all(b" ")?;
            }
            attribute.output(writer)?;
        }
        writer.write_all(b">")?;

        // Outputs childnodes of Element.
        let list = (*element_inner.children.list).borrow();
        for node in list.iter() {
            node.output(writer)?;
        }

        // Outputs end tag.
        write_seq!(writer, b"</", element_inner.name.as_bytes(), b">");
        Ok(())
    }
}

impl CompactOutput for Text {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        let text_inner = (*self.inner).borrow();
        write_content_replace_escape(writer, text_inner.val.clone())?;
        Ok(())
    }
}

impl CompactOutput for Comment {
    fn output<W: Write>(&self, _writer: &mut W) -> Result<(), XmlError> {
        Ok(())
    }
}

impl CompactOutput for CdataSection {
    fn output<W: Write>(&self, writer: &mut W) -> Result<(), XmlError> {
        let cdata_inner = (*self.inner).borrow();
        write_seq!(writer, b"<![CDATA[", cdata_inner.val.as_bytes(), b"]]>");
        Ok(())
    }
}

pub(crate) fn write_content_replace_escape<W: Write>(
    writer: &mut W,
    string: String,
) -> Result<(), XmlError> {
    let mut slice_start = 0;
    let mut slice_len = 0;

    for char in string.chars() {
        match char {
            '<' => {
                write_seq!(
                    writer,
                    &string.as_bytes()[slice_start..slice_start + slice_len],
                    b"&lt;"
                );
                slice_start += slice_len + '<'.len_utf8();
                slice_len = 0;
            }
            '>' => {
                write_seq!(
                    writer,
                    &string.as_bytes()[slice_start..slice_start + slice_len],
                    b"&gt;"
                );
                slice_start += slice_len + '>'.len_utf8();
                slice_len = 0;
            }
            '&' => {
                write_seq!(
                    writer,
                    &string.as_bytes()[slice_start..slice_start + slice_len],
                    b"&amp;"
                );
                slice_start += slice_len + '&'.len_utf8();
                slice_len = 0;
            }
            '\'' => {
                write_seq!(
                    writer,
                    &string.as_bytes()[slice_start..slice_start + slice_len],
                    b"&apos;"
                );
                slice_start += slice_len + '\''.len_utf8();
                slice_len = 0;
            }
            '\"' => {
                write_seq!(
                    writer,
                    &string.as_bytes()[slice_start..slice_start + slice_len],
                    b"&quot;"
                );
                slice_start += slice_len + '\"'.len_utf8();
                slice_len = 0;
            }
            _ch => {
                slice_len += char.len_utf8();
            }
        }
    }
    if slice_start < string.as_bytes().len() {
        writer.write_all(&string.as_bytes()[slice_start..])?;
    }
    Ok(())
}

#[cfg(test)]
mod ut_compact_output {
    use super::AuxiliaryWriter;
    use crate::{CompactOutput, Document};

    /// UT test for `Document::output`.
    ///
    /// # Title
    /// ut_output_xml_simple
    ///
    /// # Brief
    /// 1. Creates a simple xml instance.
    /// 2. Calls `Document::from_text` and `Document::output`.
    /// 3. Checks if the test results are correct.
    #[test]
    fn ut_output_xml_simple() {
        const SIMPLE_XML: &str = r#"
        <?xml version="1.0" encoding="utf-8"?>
        <note>
        <to>Tove</to>
        <from>Jani</from>
        </note>
        "#;
        let document = Document::from_text(SIMPLE_XML).unwrap();
        let expected =
            r#"<?xml version="1.0" encoding="UTF-8"?><note><to>Tove</to><from>Jani</from></note>"#;
        let mut writer = AuxiliaryWriter::new();
        document.output(&mut writer).unwrap();
        let serialize_result = unsafe { String::from_utf8_unchecked(writer.output) };
        assert_eq!(serialize_result, expected);
    }

    /// UT test for `Document::output`.
    ///
    /// # Title
    /// ut_output_xml_complex
    ///
    /// # Brief
    /// 1. Creates a complex xml instance.
    /// 2. Calls `Document::to_compact_string` to output the xml instance.
    /// 3. Checks if the test results are correct.
    #[test]
    fn ut_output_xml_complex() {
        const COMPLEX_XML: &str = r#"
        <?xml version="1.0" encoding="utf-8" standalone="yes"?>
        <!-- complex xml text -->
        <project name="project-name">
            <libraries>
                <library groupId="org.example" artifactId="&lt;name&gt;" version="0.1"/>
                <library groupId="com.example" artifactId="&quot;cool-lib&amp;" version="999"/>
            </libraries>
            <module name="module-1">
                <files>
                    <!-- files childnode comment -->
                    <file name="somefile.java" type="java">
                        Some &lt;java&gt; class
                    </file>
                    <file name="another_file.java" type="java">
                        Another &quot;java&quot; class
                    </file>
                    <file name="config.xml" type="xml">
                        Weird &apos;XML&apos; config
                    </file>
                </files>
                <libraries>
                    <library groupId="junit" artifactId="junit" version="1.9.5"/>
                </libraries>
            </module>
            <module name="module-2">
                <files>
                    <file name="program.js" type="javascript">
                        JavaScript &amp; program
                    </file>
                    <file name="style.css" type="css">
                        Cascading style sheet: &#xA9; - &#1161;
                    </file>
                </files>
            </module>
        </project>
        "#;
        let document = Document::from_text(COMPLEX_XML).unwrap();
        let expected = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><project name="project-name"><libraries><library artifactId="&lt;name&gt;" groupId="org.example" version="0.1"></library><library artifactId="&quot;cool-lib&amp;" groupId="com.example" version="999"></library></libraries><module name="module-1"><files><file name="somefile.java" type="java">Some &lt;java&gt; class</file><file name="another_file.java" type="java">Another &quot;java&quot; class</file><file name="config.xml" type="xml">Weird &apos;XML&apos; config</file></files><libraries><library artifactId="junit" groupId="junit" version="1.9.5"></library></libraries></module><module name="module-2"><files><file name="program.js" type="javascript">JavaScript &amp; program</file><file name="style.css" type="css">Cascading style sheet: © - ҉</file></files></module></project>"#;
        assert_eq!(document.to_compact_string().unwrap(), expected);
    }

    /// UT test for `write_content_replace_escape`.
    ///
    /// # Title
    /// ut_output_xml_reference
    ///
    /// # Brief
    /// 1. Creates a xml instance contains escape characters.
    /// 2. Calls `Document::from_text` and `Document::output`.
    /// 3. Checks if the test results are correct.
    #[test]
    fn ut_output_xml_escape() {
        const REFERENCE_XML: &str = r#"
        <?xml version="1.0" encoding="utf-8"?>
        <note>
        <file1>Some &lt;java&gt; class</file1>
        <file2>Another &quot;java&quot; class</file2>
        <file3>Cascading style sheet: &#xA9; - &#1161;</file3>
        </note>
        "#;
        let document = Document::from_text(REFERENCE_XML).unwrap();
        let expected = r#"<?xml version="1.0" encoding="UTF-8"?><note><file1>Some &lt;java&gt; class</file1><file2>Another &quot;java&quot; class</file2><file3>Cascading style sheet: © - ҉</file3></note>"#;
        let mut writer = AuxiliaryWriter::new();
        document.output(&mut writer).unwrap();
        let serialize_result = unsafe { String::from_utf8_unchecked(writer.output) };
        assert_eq!(serialize_result, expected);
    }

    /// UT test for `CdataSection::output`.
    ///
    /// # Title
    /// ut_output_xml_cdata_section
    ///
    /// # Brief
    /// 1. Creates a xml instance contains CDATA section.
    /// 2. Calls `Document::from_text` and `Document::output`.
    /// 3. Checks if the test results are correct.
    #[test]
    fn ut_output_xml_cdata_section() {
        const CDATA_XML: &str = r#"
        <?xml version="1.0" encoding="utf-8"?>
        <note>
        <script><![CDATA[
        function matchwo(a,b)
        { if (a < b && a < 0)
        then { return 1;}
        else { return 0;}
        }
        ]]></script>
        </note>
        "#;
        let document = Document::from_text(CDATA_XML).unwrap();
        let expected = r#"<?xml version="1.0" encoding="UTF-8"?><note><script><![CDATA[
        function matchwo(a,b)
        { if (a < b && a < 0)
        then { return 1;}
        else { return 0;}
        }
        ]]></script></note>"#;
        let mut writer = AuxiliaryWriter::new();
        document.output(&mut writer).unwrap();
        let serialize_result = unsafe { String::from_utf8_unchecked(writer.output) };
        assert_eq!(serialize_result, expected);
    }
}
